/* 
 * Copyright 2010 Ziya Guven Koylu, Ozge Koylu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.koylu.caffein.model.handler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javassist.util.proxy.MethodHandler;

import org.apache.commons.beanutils.PropertyUtils;

import com.koylu.caffein.model.caffein.CaffeinConfig;
import com.koylu.caffein.model.clazz.Clazz;
import com.koylu.caffein.model.clazz.id.Id;
import com.koylu.caffein.model.container.ClazzContainer;
import com.koylu.caffein.query.Query;

public class ManyToManyContainerHandler implements MethodHandler {

	private CaffeinConfig caffeinConfig;
	private Clazz foreignClazz;
	private Id id;
	private Class<? extends Collection<?>> returnType;
	private Query query;
	private Collection<?> lazyLoadedCollection;
	private Map<String, Object> addedObjects = new HashMap<String, Object>();
	private Map<String, Object> removedObjects = new HashMap<String, Object>();
	private Integer size;
	private boolean indexChanged = false;

	public ManyToManyContainerHandler(CaffeinConfig caffeinConfig, Clazz foreignClazz, Class<? extends Collection<?>> returnType, Query query) {
		this.caffeinConfig = caffeinConfig;
		this.foreignClazz = foreignClazz;
		this.id = foreignClazz.getId();
		this.returnType = returnType;
		this.query = query;
	}

	public Object invoke(Object object, Method method, Method proceed, Object[] args) throws Throwable {
		String methodName = method.getName();
		if ("getQuery".equals(methodName)) {
			return query;
		} else if ("getAddedObjects".equals(methodName)) {
			return addedObjects.values();
		} else if ("getRemovedObjects".equals(methodName)) {
			return removedObjects.values();
		} else if ("size".equals(methodName)) {
			if (lazyLoadedCollection == null) {
				if (size == null) {
					size = query.count();
				}
				return size;
			}
			return lazyLoadedCollection.size();
		} else if ("getContainerCollection".equals(methodName)) {
			return lazyLoadedCollection;
		} else if ("trackIdChanges".equals(methodName)) {
			if (lazyLoadedCollection != null) {
				for (Object element : lazyLoadedCollection) {
					ClazzContainer clazzContainer = (ClazzContainer) element;
					if (clazzContainer.isContainerIdUpdated()) {
						//add new id to the addedList
						objectAdded(clazzContainer);
						//add old id to the removedList
						Object removedObject = foreignClazz.getRealClass(caffeinConfig).newInstance();
						PropertyUtils.setProperty(removedObject, id.getName(), clazzContainer.getContainerId());
						objectRemoved(removedObject);
					}
				}
			}
			return null;
		} else if ("resetContainer".equals(methodName)) {
			removedObjects.clear();
			addedObjects.clear();
			lazyLoadedCollection = null;
			size = null;
			indexChanged = false;
			return null;
		} else if ("isIndexChanged".equals(methodName)) {
			return indexChanged;
		}
		if (lazyLoadedCollection == null) {
			lazyLoadedCollection = query.collection(returnType);
		}
		if("add".equals(methodName)){
			Object result = Boolean.FALSE;
			if (args.length == 1) {//boolean add(Object o)
				if(args[0] != null){
					result = method.invoke(lazyLoadedCollection, args);
					if((Boolean)result){
						objectAdded(args[0]);
					}
				}
			} else {//void add(int index, Object element) 
				if(args[1] != null){
					result = method.invoke(lazyLoadedCollection, args);
					objectAdded(args[1]);
				}
			}
			return result;
		} else if("addAll".equals(methodName)){
			if (args.length == 1) {//boolean addAll(Collection c)
				if(args[0] != null){
					boolean result = Boolean.FALSE;
					Collection<Object> target = (Collection<Object>)object;
					for (Iterator<Object> iterator = ((Collection<Object>)args[0]).iterator(); iterator.hasNext();) {
						if((Boolean)target.add(iterator.next())){
							result = Boolean.TRUE;
						}
					}
					return result;
				}
			}else{//boolean addAll(int index, Collection c)
				if(args[1] != null){
					int sizeOfLazyLoadedCollectionAtBeginning = lazyLoadedCollection.size();
					boolean result = Boolean.FALSE;
					Integer begin = (Integer)args[0];
					List<Object> target = (List<Object>)object;
					Collection<Object> source = (Collection<Object>)args[1];
					int k = 0;
					for (Iterator<Object> iterator = source.iterator(); iterator.hasNext();) {
						target.add(begin+k++, iterator.next());
					}
					if(sizeOfLazyLoadedCollectionAtBeginning != lazyLoadedCollection.size()){
						result = Boolean.TRUE;
					}
					return result;
				}
			}
			return method.invoke(lazyLoadedCollection, args);
		} else if ("clear".equals(methodName)) {//void clear()
			List<Object> temp = new ArrayList<Object>();
			for (Iterator<?> iterator = lazyLoadedCollection.iterator(); iterator.hasNext();) {
				temp.add(iterator.next());
			}
			Object result = method.invoke(lazyLoadedCollection, args);
			collectionRemoved(temp);			
			return result;
		} else if("remove".equals(methodName)){
			Object result = method.invoke(lazyLoadedCollection, args);
			if (args[0] instanceof Integer) {//Object remove(int index)
				objectRemoved(result);
			}else{//boolean remove(Object o) 
				if((Boolean)result){
					objectRemoved(args[0]);
				}
			}
			return result;
		} else if("removeAll".equals(methodName)){//boolean removeAll(Collection c)
			if(args[0] != null){
				boolean result = Boolean.FALSE;
				Collection<Object> target = (Collection<Object>)object;
				for (Iterator<Object> iterator = ((Collection<Object>)args[0]).iterator(); iterator.hasNext();) {
					if((Boolean)target.remove(iterator.next())){
						result = Boolean.TRUE;
					}
				}
				return result;
			}
			return method.invoke(lazyLoadedCollection, args);
		} else if("set".equals(methodName)){//Object	set(int index, Object element)
			Object result = null;
			if(args[1] != null){
				result = method.invoke(lazyLoadedCollection, args);
				try {
					objectAdded(args[1]);
					objectRemoved(result);	
				} catch (Exception e) {
					args[1] = result;
					method.invoke(lazyLoadedCollection, args);
				}
			}
			return result;	
		} else if("iterator".equals(methodName)){//iterator
			return new ManyToManyContainerHandlerIterator(this, (Iterator<Object>)method.invoke(lazyLoadedCollection, args));
		} else if("retainAll".equals(methodName)){//boolean retainAll(Collection c)
			if(args[0] != null){
				int sizeOfLazyLoadedCollectionAtBeginning = lazyLoadedCollection.size();
				boolean result = Boolean.FALSE;
				Set<Object> temp = new HashSet<Object>();
				temp.addAll((Collection<Object>)args[0]);
				for (Iterator<?> iterator = lazyLoadedCollection.iterator(); iterator.hasNext();) {
					Object current = iterator.next();
					if( (Boolean)temp.add(current) ){
						iterator.remove();
						objectRemoved(current);
					}
				}
				if(sizeOfLazyLoadedCollectionAtBeginning != lazyLoadedCollection.size()){
					result = Boolean.TRUE;
				}
				return result;
			}
		}
		return method.invoke(lazyLoadedCollection, args);
	}
	
	private void collectionAdded(Collection<?> collection) throws Exception{
		for (Iterator<?> iterator = collection.iterator(); iterator.hasNext();) {
			objectAdded(iterator.next());
		}
	}
	
	private void objectAdded(Object object) throws Exception{
		if( !(object instanceof ClazzContainer) ){
			throw new RuntimeException("invalid object load it from caffein first");
		}
		String key = id.toUniqueId(caffeinConfig, PropertyUtils.getProperty(object, id.getName()));
		if (removedObjects.containsKey(key)) {
			removedObjects.remove(key);
		} else {
			addedObjects.put(key, object);
		}
		indexChanged = true;
	}
	
	private void collectionRemoved(Collection<?> collection) throws Exception{
		for (Iterator<?> iterator = collection.iterator(); iterator.hasNext();) {
			objectRemoved(iterator.next());
		}
	}
	
	private void objectRemoved(Object object) throws Exception{
		String key = id.toUniqueId(caffeinConfig, PropertyUtils.getProperty(object, id.getName()));
		if (addedObjects.containsKey(key)) {
			addedObjects.remove(key);
		} else {
			removedObjects.put(key, object);
		}
		indexChanged = true;
	}

	public void objectRemovedFromIterator(Object object) {
		try {
			objectRemoved(object);			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static class ManyToManyContainerHandlerIterator implements Iterator<Object>{
		private ManyToManyContainerHandler manyToManyContainerHandler = null;
		private Iterator<Object> iterator = null;
		private Object current = null;
		
		public ManyToManyContainerHandlerIterator(ManyToManyContainerHandler manyToManyContainerHandler, Iterator<Object> iterator) {
			this.manyToManyContainerHandler = manyToManyContainerHandler;
			this.iterator = iterator;
		}
		
		public boolean hasNext() {
			return iterator.hasNext();
		}
		
		public Object next() {
			current = iterator.next();
			return current;
		}
		
		public void remove() {
			iterator.remove();
			manyToManyContainerHandler.objectRemovedFromIterator(current);
		}
	}
}